﻿using System;
using System.Data;
using System.Data.Common;
using System.Diagnostics;

namespace Dare.Utilities.Data
{
    /// <summary>
    /// TransactionManager is utility class that decorates a <see cref="IDbTransaction"/> instance.
    /// </summary>
    public class TransactionManager : IDisposable
    {
        #region Fields
        private DbProviderFactory _factory;
        private DbConnection _connection;
        private DbTransaction _transaction;

        private string _connectionString;
        private bool _transactionOpen = false;
        #endregion

        /// <summary>
        ///		Initializes a new instance of the <see cref="TransactionManager"/> class.
        /// </summary>
        internal TransactionManager()
        {
        }

        /// <summary>
        ///		Initializes a new instance of the <see cref="TransactionManager"/> class.
        /// </summary>
        /// <param name="connectionString">The connection string to the database.</param>
        public TransactionManager(string connectionString)
            : this(connectionString, "System.Data.SqlClient")
        {
        }

        /// <summary>
        ///		Initializes a new instance of the <see cref="TransactionManager"/> class.
        /// </summary>
        /// <param name="connectionString">The connection string to the database.</param>
        /// <param name="providerInvariantName">Name of the provider invariant.</param>
        public TransactionManager(string connectionString, string providerInvariantName)
        {
            this._connectionString = connectionString;
            this._factory = DbUtil.GetDbProviderFactory(providerInvariantName);
            this._connection = this._factory.CreateConnection();
            this._connection.ConnectionString = connectionString;
        }

        public TransactionManager(string connectionString, string assemblyFile, string factoryClassName)
        {
            this._connectionString = connectionString;
            this._factory = DbUtil.GetDbProviderFactoryFrom(assemblyFile, factoryClassName);
            this._connection = this._factory.CreateConnection();
            this._connection.ConnectionString = connectionString;
        }

        public TransactionManager(string connectionString, DbProviderFactory dbFactory)
        {
            this._connectionString = connectionString;
            this._factory = dbFactory;
            this._connection = this._factory.CreateConnection();
            this._connection.ConnectionString = connectionString;
        }

        /// <summary>
        ///		Gets or sets the configuration key for database service.
        /// </summary>
        /// <remark>Do not change during a transaction.</remark>
        /// <exception cref="InvalidOperationException">
        /// If an attempt to set when the connection is currently open.
        /// </exception>
        public string ConnectionString
        {
            get { return this._connectionString; }
            set
            {//make sure transaction is open
                if (this.IsOpen)
                    throw new InvalidOperationException("Database cannot be changed during a transaction");

                this._connectionString = value;
                if (this._connectionString.Length > 0 && this._connection != null)
                {
                    this._connection.ConnectionString = _connectionString;
                }
            }
        }

        public DbProviderFactory DbProviderFactory
        {
            get { return this._factory; }
            set
            {
                if (this.IsOpen)
                    throw new InvalidOperationException("Database cannot be changed during a transaction");

                this._factory = value;
                if (_factory != null)
                {
                    this._connection = _factory.CreateConnection();
                    this._connection.ConnectionString = this._connectionString;
                }
                else
                {
                    this._connection = null;
                }
            }
        }

        private string name;
        public string Name
        {
            get { return name; }
            set { name = value; }
        }

        public override string ToString()
        {
            return String.IsNullOrEmpty(name) ? base.ToString() : name;
        }

        /// <summary>
        ///	Gets the underlying <see cref="DbTransaction"/> object.
        /// </summary>
        public DbTransaction TransactionObject
        {
            get { return this._transaction; }
        }

        /// <summary>
        ///	Gets a value that indicates if a transaction is currently open and operating. 
        /// </summary>
        /// <value>Return true if a transaction session is currently open and operating; otherwise false.</value>
        public bool IsOpen
        {
            get { return this._transactionOpen; }
        }

        /// <summary>
        ///		Begins a transaction.
        /// </summary>
        /// <remarks>The default <see cref="IsolationLevel"/> mode is ReadCommitted</remarks>
        /// <exception cref="InvalidOperationException">If a transaction is already open.</exception>
        public void BeginTransaction()
        {
            BeginTransaction(IsolationLevel.ReadCommitted);
        }

        /// <summary>
        ///		Begins a transaction.
        /// </summary>
        /// <param name="isolationLevel">The <see cref="IsolationLevel"/> level of the transaction</param>
        /// <exception cref="InvalidOperationException">If a transaction is already open.</exception>
        /// <exception cref="DataException"></exception>
        /// <exception cref="DbException"></exception>
        public void BeginTransaction(IsolationLevel isolationLevel)
        {
            if (IsOpen)
                throw new InvalidOperationException("Transaction already open.");

            //Open connection
            try
            {
                this._connection.Open();
                this._transaction = this._connection.BeginTransaction(isolationLevel);
                this._transactionOpen = true;
            }
            catch (Exception ex)
            {
                // in the event of an error, close the connection and destroy the transobject.
                if (this._connection != null) this._connection.Close();
                if (this._transaction != null) this._transaction.Dispose();
                this._transactionOpen = false;
                throw new TransactionManagerException(TransactionManagerException.ERROR_CODE_DB_CONNECT_FAILED, ex);
            }
        }

        /// <summary>
        ///		Commit the transaction to the datasource.
        /// </summary>
        /// <exception cref="InvalidOperationException">If a transaction is not open.</exception>
        public void Commit()
        {
            if (!this.IsOpen)
            {
                throw new InvalidOperationException("Transaction needs to begin first.");
            }

            try
            {
                this._transaction.Commit(); // SqlClient could throw Exception or InvalidOperationException
            }
            finally
            {
                //assuming the commit was sucessful.
                this._connection.Close();
                this._transaction.Dispose();
                this._transactionOpen = false;
            }
        }

        /// <summary>
        ///	Rollback the transaction.
        /// </summary>
        /// <exception cref="InvalidOperationException">If a transaction is not open.</exception>
        public void Rollback()
        {
            if (!this.IsOpen)
            {
                throw new InvalidOperationException("Transaction needs to begin first.");
            }

            try
            {
                this._transaction.Rollback(); // SqlClient could throw Exception or InvalidOperationException
            }
            finally
            {
                this._connection.Close();
                this._transaction.Dispose();
                this._transactionOpen = false;
            }
        }

        private bool disposed;
        private static object syncRoot = new object();

        /// <summary>
        /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
        /// </summary>
        public void Dispose()
        {
            if (!disposed)
            {
                lock (syncRoot)
                {
                    disposed = true;

                    if (this.IsOpen)
                    {
                        this.Rollback();
                    }
                }
            }
        }
    }
}
